package com.tq.framework.context.annotation;

import com.tq.framework.annotation.Component;
import com.tq.framework.beans.BeanDefinition;
import lombok.extern.slf4j.Slf4j;

import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Enumeration;
import java.util.Set;
import java.util.HashSet;
import java.util.jar.JarFile;

/**
 * 类路径扫描器，用于扫描指定包下的候选组件
 * 参考Spring的ClassPathScanningCandidateComponentProvider设计
 *
 * @author TqCoder
 * @since 2.0.0
 */
@Slf4j
public class ClassPathScanningCandidateComponentProvider {

    private static final String CLASS_FILE_SUFFIX = ".class";
    private static final String PACKAGE_SEPARATOR = ".";
    private static final String PATH_SEPARATOR = "/";
    private static final String FILE_PROTOCOL = "file";
    private static final String JAR_PROTOCOL = "jar";
    private static final String JAR_URL_SEPARATOR = "!";
    private static final String FILE_URL_PREFIX = "file:";

    /**
     * 扫描指定包下的所有候选组件
     *
     * @param basePackage 基础包名
     * @return 扫描到的BeanDefinition集合
     */
    public Set<BeanDefinition> findCandidateComponents(String basePackage) throws IOException {
        log.info("开始扫描包：{}", basePackage);
        Set<BeanDefinition> candidates = new HashSet<>();
        Set<Class<?>> classes = scanPackage(basePackage);

        classes.stream().filter(clazz -> clazz.isAnnotationPresent(Component.class))
                .peek(clazz -> log.info("找到候选组件：{}", clazz.getName()))
                .forEach(clazz -> candidates.add(new BeanDefinition(clazz)));
        return candidates;
    }

    private Set<Class<?>> scanPackage(String packageName) throws IOException {
        Set<Class<?>> classes = new HashSet<>();
        String pathName = packageName.replace(PACKAGE_SEPARATOR, PATH_SEPARATOR);
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        
        try {
            Enumeration<URL> resources = classLoader.getResources(pathName);
            while (resources.hasMoreElements()) {
                URL resource = resources.nextElement();
                
                if (FILE_PROTOCOL.equals(resource.getProtocol())) {
                    Path rootDir = Paths.get(resource.toURI());
                    scanFileSystem(rootDir, packageName, classes);
                } else if (JAR_PROTOCOL.equals(resource.getProtocol())) {
                    scanJar(resource, pathName, classes);
                }
            }
        } catch (URISyntaxException e) {
            throw new IOException("Invalid package path", e);
        }
        
        return classes;
    }
    
    /**
     * 扫描文件系统中的类文件
     * 该方法用于开发环境（如IDE中）扫描类文件，此时类文件直接存在于文件系统中（通常在target/classes目录下）
     * 使用Files.walkFileTree遍历目录树，查找所有.class文件
     *
     * @param rootDir 要扫描的根目录路径
     * @param packageName 包名
     * @param classes 用于存储扫描到的类的集合
     * @throws IOException 当IO操作发生异常时抛出
     */
    private void scanFileSystem(Path rootDir, String packageName, Set<Class<?>> classes) throws IOException {
        Files.walkFileTree(rootDir, new SimpleFileVisitor<>() {
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
                if (file.toString().endsWith(CLASS_FILE_SUFFIX)) {
                    String relativePath = rootDir.relativize(file).toString();
                    String className = packageName + PACKAGE_SEPARATOR + relativePath
                            .replace(File.separator, PACKAGE_SEPARATOR)
                            .substring(0, relativePath.length() - CLASS_FILE_SUFFIX.length());
                    
                    tryLoadClass(className, classes);
                }
                return FileVisitResult.CONTINUE;
            }
        });
    }
    
    /**
     * 扫描JAR包中的类文件
     * 该方法用于生产环境，此时类文件被打包在JAR文件中
     * 使用JarFile API遍历JAR包内的条目，查找所有.class文件
     *
     * @param resource JAR文件的URL资源
     * @param pathName 要扫描的包路径
     * @param classes 用于存储扫描到的类的集合
     * @throws IOException 当IO操作发生异常时抛出
     */
    private void scanJar(URL resource, String pathName, Set<Class<?>> classes) throws IOException {
        String jarPath = resource.getPath().substring(FILE_URL_PREFIX.length(), resource.getPath().indexOf(JAR_URL_SEPARATOR));
        
        try (JarFile jar = new JarFile(jarPath)) {
            jar.stream()
               .filter(entry -> !entry.isDirectory() 
                              && entry.getName().startsWith(pathName)
                              && entry.getName().endsWith(CLASS_FILE_SUFFIX))
               .forEach(entry -> {
                   String entryName = entry.getName();
                   String className = entryName.substring(0, entryName.length() - CLASS_FILE_SUFFIX.length())
                           .replace(PATH_SEPARATOR, PACKAGE_SEPARATOR);
                   tryLoadClass(className, classes);
               });
        }
    }
    
    private void tryLoadClass(String className, Set<Class<?>> classes) {
        try {
            classes.add(Class.forName(className));
        } catch (ClassNotFoundException | NoClassDefFoundError e) {
            // 忽略无法加载的类
            log.warn("无法加载类：{}", className);
        }
    }
} 